﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using Commands;
using Commands.Dialogs;
using ImageConverter.Data;
using ImageConverter.Data.Containers;
using ImageConverter.Model;
using ImageConverter.Model.Enums;
using ImageConverter.Utils;
using ImageConverter.Utils.Bitmap;
using SevenZip;

namespace ImageConverter.ViewModels
{
    public class RecodeViewModel : ViewModelBase, IRecodeViewModel
    {
        public RecodeViewModel()
        {
            PauseRecodeCommand = new DelegateCommand<object>(OnPauseRecodeCommand);
            ResumeRecodeCommand = new DelegateCommand<object>(OnResumeRecodeCommand);
            CancelRecodeCommand = new DelegateCommand<object>(OnCancelRecodeCommand);
            MinimizeRecodeViewCommand = new DelegateCommand<object>(OnMinimizeRecodeViewCommand);

            ContainerFiltersList = new List<ContainerFilterEnum>(
                Enum.GetValues(typeof(ContainerFilterEnum)).Cast<ContainerFilterEnum>()
                );
            SelectedFilter = ContainerFilterEnum.All;
        }

        #region Properties

        public int BaseLength { get; private set; }

        public IEntryContainerFactory ContainerFactory { get; private set; }

        public bool RecodingLastContainer { get; private set; }

        public bool RecodedLastContainer { get; private set; }

        public bool IsRecoding { get; private set; }

        public bool CompressOutput { get; private set; }

        #endregion

        #region IRecodeViewModel Members

        public ObservableCollection<ContainerItem> Containers
        {
            get
            {
                return containers;
            }
            private set
            {
                containers = value;
                NotifyOfPropertyChange(() => Containers);
            }
        }

        public ContainerItem SelectedContainer
        {
            get
            {
                return selectedContainer;
            }
            set
            {
                selectedContainer = value;
                NotifyOfPropertyChange(() => SelectedContainer);
            }
        }

        public IEnumerable<ContainerFilterEnum> ContainerFiltersList
        {
            get
            {
                return containerFiltersList;
            }
            private set
            {
                containerFiltersList = value;
                NotifyOfPropertyChange(() => ContainerFiltersList);
            }
        }

        public ContainerFilterEnum SelectedFilter
        {
            get
            {
                return selectedFilter;
            }
            set
            {
                selectedFilter = value;
                NotifyOfPropertyChange(() => SelectedFilter);

                OnSelectedFilterChanged(value);
            }
        }

        public string OutputDir
        {
            get
            {
                return outputDir;
            }
            private set
            {
                outputDir = value;
                NotifyOfPropertyChange(() => OutputDir);
            }
        }

        public double MinProgressValue
        {
            get
            {
                return 0d;
            }
        }

        public double MaxProgressValue
        {
            get
            {
                return 100d;
            }
        }

        public double CurrentProgressValue
        {
            get
            {
                return currentProgressValue;
            }
            set
            {
                currentProgressValue = value;
                NotifyOfPropertyChange(() => CurrentProgressValue);
            }
        }

        public double TotalProgressValue
        {
            get
            {
                return totalProgressValue;
            }
            set
            {
                totalProgressValue = value;
                NotifyOfPropertyChange(() => TotalProgressValue);
            }
        }

        public double CurrentProgressDelta
        {
            get
            {
                return currentProgressDelta;
            }
            private set
            {
                currentProgressDelta = value;
            }
        }

        public double TotalProgressDelta
        {
            get
            {
                return totalProgressDelta;
            }
            private set
            {
                totalProgressDelta = value;
            }
        }

        public int CurrentEntryIndex
        {
            get
            {
                return currentEntryIndex;
            }
            private set
            {
                currentEntryIndex = value;
                NotifyOfPropertyChange(() => CurrentEntryIndex);
            }
        }

        public int TotalEntriesInContainer
        {
            get
            {
                return totalEntriesInContainer;
            }
            private set
            {
                totalEntriesInContainer = value;
                NotifyOfPropertyChange(() => TotalEntriesInContainer);
            }
        }

        public int CurrentContainerIndex
        {
            get
            {
                return currentContainerIndex;
            }
            private set
            {
                currentContainerIndex = value;
                NotifyOfPropertyChange(() => CurrentContainerIndex);
            }
        }

        public int TotalContainers
        {
            get
            {
                return totalContainers;
            }
            private set
            {
                totalContainers = value;
                NotifyOfPropertyChange(() => TotalContainers);
            }
        }

        public void BeginRecode(IEnumerable<ContainerItem> containers, IEntryContainerFactory containerFactory, int baseLegth, string outputDir, bool compressOutput)
        {
            if (containers == null || containerFactory == null)
                throw new NullReferenceException("Illegal null-reference args.");

            BaseLength = baseLegth;
            ContainerFactory = containerFactory;
            Containers = containers.ToObservableCollection();
            OutputDir = outputDir;
            CompressOutput = compressOutput;
            StartRecode();
        }

        public bool? IsCanceled
        {
            get
            {
                return isCanceled;
            }
            private set
            {
                isCanceled = value;
                NotifyOfPropertyChange(() => IsCanceled);
            }
        }

        public bool IsPaused
        {
            get
            {
                return isPaused;
            }
            private set
            {
                isPaused = value;
                NotifyOfPropertyChange(() => IsPaused);
            }
        }

        public bool IsCompleted
        {
            get
            {
                return isCompleted;
            }
            private set
            {
                isCompleted = value;
                NotifyOfPropertyChange(() => IsCompleted);
            }
        }

        public DelegateCommand<object> PauseRecodeCommand { get; private set; }

        public DelegateCommand<object> CancelRecodeCommand { get; private set; }

        public DelegateCommand<object> ResumeRecodeCommand { get; private set; }

        public DelegateCommand<object> MinimizeRecodeViewCommand { get; private set; }

        #endregion

        #region Private methods

        private void OnSelectedFilterChanged(ContainerFilterEnum filter)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(Containers);
            if (view != null)
            {
                switch (filter)
                {
                    case ContainerFilterEnum.All:
                        view.Filter = null;
                        break;
                    case ContainerFilterEnum.Success:
                        view.Filter = (o) =>
                        {
                            return o is ContainerItem && ((ContainerItem)o).Entries.All(e => e.Status == ItemStatusEnum.Success);
                        };
                        break;
                    case ContainerFilterEnum.Error:
                        view.Filter = (o) =>
                        {
                            return o is ContainerItem && ((ContainerItem)o).Entries.Any(e => e.Status == ItemStatusEnum.Error);
                        };
                        break;
                    case ContainerFilterEnum.Warning:
                        view.Filter = (o) =>
                        {
                            return o is ContainerItem && ((ContainerItem)o).Entries.Any(e => e.Status == ItemStatusEnum.Warning);
                        };
                        break;
                    case ContainerFilterEnum.WarningOrError:
                        view.Filter = (o) =>
                        {
                            return o is ContainerItem && ((ContainerItem)o).Entries.
                                Any(e => e.Status == ItemStatusEnum.Warning || e.Status == ItemStatusEnum.Error);
                        };
                        break;
                }
            }
        }

        private void OnPauseRecodeCommand(object parameter)
        {
            IsPaused = true;
            pauseToken.IsPaused = IsPaused;
        }

        private void OnResumeRecodeCommand(object parameter)
        {
            IsPaused = false;
            pauseToken.IsPaused = IsPaused;
        }

        private void OnCancelRecodeCommand(object parameter)
        {
            if (cancelTokenSource == null)
                return;

            if (IsRecoding)
            {
                ConfirmWindow wnd = new ConfirmWindow()
                {
                    DialogText = "Conversion is in progress. Do you really want to cancel?"
                };
                if (wnd.ShowDialog() == true)
                {
                    CancelRecodeImpl();
                }
            }
            else
            {
                CancelRecodeImpl();
            }
        }

        private void CancelRecodeImpl()
        {
            cancelTokenSource.Cancel();
            IsCanceled = true;
        }

        private void OnMinimizeRecodeViewCommand(object parameter)
        {
            Window wnd = parameter as Window;
            if (wnd == null)
                return;

            wnd.WindowState = WindowState.Minimized;
        }

        private void StartRecode()
        {
            IsRecoding = true;

            RecodeArchivesCollection();
        }

        private void RecodeArchivesCollection()
        {
            IEntryReader entryContainer = ContainerFactory.Create();

            cancelTokenSource = new CancellationTokenSource();
            pauseToken = new PauseToken();


            TotalContainers = Containers.Count;
            CurrentContainerIndex = 0;
            TotalProgressValue = MinProgressValue;
            TotalProgressDelta = MaxProgressValue / TotalContainers;
            IsCanceled = null;
            IsPaused = false;
            IsCompleted = false;

            Task recodeTask = Task.Factory.StartNew(() =>
                {
                    Queue<ContainerItem> recodeQueue = new Queue<ContainerItem>(Containers);

                    while (recodeQueue.Count > 0)
                    {
                        if (isRecoding)
                            Thread.Sleep(500);
                        else
                        {
                            if (recodeQueue.Count == 1)
                            {
                                RecodingLastContainer = true;
                                RecodedLastContainer = false;
                            }

                            ContainerItem currentItem = recodeQueue.Dequeue();
                            if (!entryContainer.ValidateEntries(currentItem.FullName))
                            {
                                currentItem.Status = ItemStatusEnum.Error;
                                ++CurrentContainerIndex;
                                TotalProgressValue += TotalProgressDelta;

                                if (recodeQueue.Count == 0) // last item error
                                {
                                    RecodedLastContainer = true;
                                }
                            }
                            else
                            {
                                var entryNames = entryContainer.GetArchiveEntries(currentItem.FullName);

                                TotalEntriesInContainer = entryNames.Count();
                                CurrentEntryIndex = 0;
                                CurrentProgressDelta = MaxProgressValue / TotalEntriesInContainer;

                                if (entryNames.Count() == 0) // guard
                                {
                                    currentItem.Status = ItemStatusEnum.Success;
                                    ++CurrentContainerIndex;
                                    TotalProgressValue += TotalProgressDelta;
                                    continue;
                                }

                                currentItem.CreateEntries(entryNames);

                                isRecoding = true;
                                currentItem.Status = ItemStatusEnum.InProgress;

                                ++CurrentContainerIndex;

                                RecodeEntries(currentItem);
                            }
                        }
                    }

                }, cancelTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

            Task recodingLastContainerTask = recodeTask.ContinueWith((t) =>
                {
                    while (!RecodedLastContainer)
                        Thread.Sleep(1000);
                }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);

            Task finishRecodeTask = recodingLastContainerTask.ContinueWith((t) =>
                {
                    ICompleteViewModel viewModel = new CompleteViewModel();
                    viewModel.Containers = Containers;

                    CompleteWindow completeDialog = new CompleteWindow(viewModel);
                    completeDialog.ShowDialog();

                    CurrentProgressValue = 0;
                    TotalProgressValue = 0;
                    IsCompleted = true;
                    IsRecoding = false;
                }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void RecodeEntries(ContainerItem item)
        {
            IEntryReader reader = ContainerFactory.Create();

            InitReader(item, reader);

            Task<ContainerResult> recodeTask = Task.Factory.StartNew<ContainerResult>(() =>
                {
                    CurrentProgressValue = MinProgressValue;

                    return reader.ExtractEntries(item.FullName, cancelTokenSource.Token, pauseToken);
                }, cancelTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

            Task<ContainerResult> compressTask = recodeTask.ContinueWith<ContainerResult>((t) =>
            {
                if (CompressOutput && t.Result.IsCompleted)
                {
                    var encodedPicturesPath = GetItemOutputPath(item);
                    var archiveName = encodedPicturesPath + ".zip";
                    var encodedFileNames = Directory.EnumerateFiles(encodedPicturesPath);

                    var compressor = new SevenZipCompressor();
                    compressor.ArchiveFormat = OutArchiveFormat.Zip;
                    compressor.CompressFiles(archiveName, encodedFileNames.ToArray());
                    Directory.Delete(encodedPicturesPath, true);
                }

                return t.Result;
            });

            Task notifyCompletedTask = compressTask.ContinueWith((t) =>
                {
                    if (t.IsCanceled)
                        return;

                    if (t.Result.IsCompleted && !t.Result.IsError)
                    {
                        bool hasErrors = item.Entries.Any(e => e.Status == ItemStatusEnum.Error);
                        bool hasWarnings = item.Entries.Any(e => e.Status == ItemStatusEnum.Warning);

                        if (!hasErrors && !hasWarnings)
                            item.Status = ItemStatusEnum.Success;
                        else
                        {
                            if (hasErrors)
                                item.Status = ItemStatusEnum.Error;
                            else
                                if (hasWarnings)
                                    item.Status = ItemStatusEnum.Warning;
                        }

                        if (RecodingLastContainer)
                            RecodedLastContainer = true;
                    }
                    else
                    {
                        item.Status = ItemStatusEnum.Error;
                        item.Error = t.Result.ErrorValue;
                    }

                    TotalProgressValue += TotalProgressDelta;
                    isRecoding = false;
                }, TaskScheduler.Default);
        }

        private void InitReader(ContainerItem item, IEntryReader reader)
        {
            reader.OnEntryRead += (ed) =>
            {
                BitmapImageEncoder encoder = new BitmapImageEncoder(BaseLength);

                var entry = item.Entries.Where(e => e.Name == Path.GetFileName(ed.FileName)).FirstOrDefault();

                entry.Status = ItemStatusEnum.InProgress;
                entry.InitialSize = ed.ByteData.LongLength;

                ++CurrentEntryIndex;

                var outPath = GetItemOutputPath(item); ;
                if (!Directory.Exists(outPath))
                    Directory.CreateDirectory(outPath);

                var encodeResult = encoder.Recode(ed, outPath);
                entry.Status = encodeResult.Result;
                entry.Dimensions = encodeResult.Target;
                entry.InitialDimensions = encodeResult.Source;
                entry.Size = encodeResult.EncodedSize;

                CurrentProgressValue += CurrentProgressDelta;

            };
            reader.OnEntryReadError += (en, ex) =>
            {
                var entry = item.Entries.Where(e => e.Name == Path.GetFileName(en)).FirstOrDefault();
                entry.Status = ItemStatusEnum.Error;

                CurrentProgressValue += CurrentProgressDelta;
            };
        }

        private string GetItemOutputPath(ContainerItem item)
        {
            string result = String.Empty;

            if (item.IsDirectory)
            {
                /*
                 * Get top folder from full path
                 * e.g. c://folder1//folder2//folder3//topFolder -> topFilder
                 */
                string topFolder = Path.GetFileName(item.FullName);

                result = Path.Combine(OutputDir, topFolder);
            }
            else
            {
                result = Path.Combine(OutputDir, Path.GetFileNameWithoutExtension(item.FullName));
            }

            return result;
        }

        #endregion

        #region Private fields

        private double currentProgressValue;
        private double totalProgressValue;
        private ObservableCollection<ContainerItem> containers;
        private IEnumerable<ContainerFilterEnum> containerFiltersList;
        private ContainerItem selectedContainer;
        private string outputDir;

        private CancellationTokenSource cancelTokenSource;
        private PauseToken pauseToken;
        private bool isRecoding;
        private double currentProgressDelta;
        private double totalProgressDelta;
        private bool? isCanceled;
        private bool isPaused;
        private bool isCompleted;
        private int currentEntryIndex;
        private int totalEntriesInContainer;
        private int currentContainerIndex;
        private int totalContainers;
        private ContainerFilterEnum selectedFilter;

        #endregion
    }
}
